
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Collections;


using Swaf;
using Swaf.Call;
using Swaf.Xml;

namespace Swaf.Container
{
	/// <summary>
	/// Summary description for XmlCatalogContainerNode.
	/// </summary>
	public class XmlCatalogContainerNode : INameAddressSupport, IMap, ICatalog
	{
		protected static CatalogEvents sm_catalogEvents = null;
		
		protected FileSystemInfo m_base;
		protected FileType m_type;
		protected string m_childType;
		protected string m_className;
		protected FlexiMap m_map;
		protected bool m_usingSpecifiedClassFactory;
		protected string m_catalogName=null;
		public string catalogName
		{
			get
			{
				return m_catalogName;
			}
			set
			{
				m_catalogName=value;
			}
		}

		public XmlElement getCatalogContents(string fullName)
		{
			NameInfo nameInfo = calculateNameInfo(fullName);
			XmlElement el = find(nameInfo, true, true) as XmlElement;
			return el;
		}

		public XmlCatalogContainerNode(Match mr, object[] extraInfo)
		{
			if(mr.Groups.Count == 2 || mr.Groups.Count == 3) 
			{
				//m_baseDir = new FileInfo(mr.Groups[1].Value);
				string loc = mr.Groups[1].Value;
				if(string.Compare(loc.Substring(0,7), "file://", true) ==0)
					loc = loc.Substring(7);
				m_type = getType(loc);
				if(m_type == FileType.NotExist)
					throw new Exception("Bad location " + loc);
				else if(m_type == FileType.Directory)
					m_base = new DirectoryInfo(loc);
				else if(m_type == FileType.File)
					m_base = new FileInfo(loc);

				m_map = new FlexiMap();

				if(mr.Groups.Count == 3)
					m_childType = mr.Groups[2].Value;
				if(extraInfo.Length > 0 && extraInfo[0] != null)
					m_className = extraInfo[0].ToString();

				if (m_className != null)
				{
					Match m = Application.currentApp.resMgr.matches("use{ws}({1+ {anychar}}){ws}factory", m_className);
					if (m.Success)
					{
						m_usingSpecifiedClassFactory = true;
						m_className = m.Groups[1].Value;
					}
				}
				else
					m_className = "";
			}
		}

        public FileSystemInfo BaseLocation { get { return m_base; } }

		protected static IDictionary CatalogEventsHandlers
		{
			get
			{
				if (sm_catalogEvents == null)
				{
					sm_catalogEvents =  Application.currentApp.appData.get("sysObjs-CatalogEvents") as CatalogEvents;
				}
				
                if(sm_catalogEvents != null)
				    return sm_catalogEvents.EventHandlers;
                return null;
			}
		}
		
		protected void cacheEntry(string fileName, string key, object entry)
		{
			if (m_childType == null || m_childType.Equals(string.Empty))
			{
				// Call any pre-cache subscribers
				IList cachingEventHdlrs = CatalogEventsHandlers[CatalogEvents.CachingCatalogItemEvent] as IList;
				if (cachingEventHdlrs != null)
				{
					foreach (ICall call in cachingEventHdlrs)
					{
						try
						{
							// gives an event handler and opportunity to massage the entry before being cached
							// this can be used for modifying the expiration if the entry is an ICatalogItem
							call.call(entry, key);
						}
						catch(Exception e)
						{
							Application.currentApp.Log.println(100, "Unable to call catalog event handler {0}, received exception {1}", call.Method, e.ToString());
						
							// purposefully don't propogate the error
						}
					}
				}				

				IExpireNode node=null;
				ICatalogItem info = entry as ICatalogItem;
				if(info != null && info.expiration != null && !info.expiration.Equals(""))
				{
					IClassFactory nodeFactory = Application.currentApp.factories["GeneralContainerNodes"];
					node = (IExpireNode) nodeFactory.createFromPattern(info.expiration, entry);
				}
				else
				{
					if (info != null)
					{
						info.expiration = "expire on change of " + fileName;
						addDefaultExpire(Application.currentApp,info);
					}
					node =  new ChangeJournalExpireNode(fileName,entry);	
				}
				
				m_map.put(key,node);
			}
			else
			{
				//If the config specifies to use no cache then don't cache the item.
				if (! m_childType.Equals("none"))
				{
					m_map.put(key, entry, m_childType);
				}
			}
		}
		#region INameAddressSupport Members

		public void put(string nameAddress, object val)
		{
			INameAddressSupport req = get(nameAddress) as INameAddressSupport;
			if (req != null && req != this)
				req.put(nameAddress, val);
		}

		public void put(string nameAddress, object val, object extraInfo)
		{
			INameAddressSupport req = get(nameAddress) as INameAddressSupport;
			if (req != null && req != this)
				req.put(nameAddress, val, extraInfo);
		}

		public object get(string nameAddress, object oneArg)
		{
			return this.get(nameAddress, new object[]{oneArg});
		}

		public object get(string nameAddress)
		{
			return this.get(nameAddress, new object[]{});
		}

		public virtual object get(string nameAddress, params object[] infoArgs)
		{
			if (nameAddress == null)
				return this;

			object[] findInfo = find(nameAddress, true);
			object val = findInfo[0];
			NameInfo nameInfo = findInfo[1] as NameInfo;
			if (val != null)
			{
				string typeName = val.GetType().Name;
				try
				{
                    string remainingName = nameInfo.RemainingName == "" ? null : nameInfo.RemainingName;
                    if (val is INameAddressSupport)
						if (infoArgs != null && infoArgs.Length > 0)
							val = ((INameAddressSupport)val).get(remainingName, infoArgs);
						else
							val = ((INameAddressSupport)val).get(remainingName);
				}
				catch (Exception e)
				{
					throw new CatalogException(
						String.Format("Exception while calling get on catalog entry '{0}.{1}' (class type {2})",
						m_catalogName, nameAddress, typeName), e);
				}
			}
			else
				throw new CatalogException(String.Format("Unable to create an instance of catalog entry '{0}.{1}'", m_catalogName, nameAddress));
			return val;
		}

		/// <summary>
		/// This method will find the catalog entry and create the object associated with it.
		/// It is guarenteed to either throw an exception or return a two element array.
		/// </summary>
		/// <remarks>
		/// With the throwException param == true, this method allows code to try and
		/// create an element, but if it fails no exception will be thrown.  Code can
		/// now "check" to see if an item exists and not have an exception thrown because
		/// it can be assumed to be normal operation if the item does not exist.  
		/// This is strikingly different from the get method which will
		/// always throw an exception if the item cannot be created or the catalog entry
		/// cannot be found.  It is up to the calling code to be responsible for the
		/// ramifications for using this method with throwException == false.  The main
		/// reason for this method is so that if there is a better then good chance the
		/// catalog entry wont be there most of the time then this is better then throwing
		/// an exception each time it is not found.</remarks>
		/// <param name="nameAddress">The deep name of the catalog entry to find.</param>
		/// <param name="throwException">If true and an exception occurs a CatalogException
		/// will be thrown.  If false, the exception info will be thrown away and the return
		/// value will be what could be returned in the two element array.  If false it is
		/// guarenteed that this method will always return a two element array.</param>
		/// <returns>A two element array.  The [0] element is the value from the catalog
		/// requested. The [1] element is the nameAddress broken down into 
		/// a NameInfo object.</returns>
		public virtual object[] find(string nameAddress, bool throwException)
		{
			object val = null;
			NameInfo nameInfo = null;
			try
			{
				IApplication app = Application.currentApp;
				string fullName = app.resMgr.resolve(nameAddress);
				nameInfo = calculateNameInfo(fullName);
				val = find(nameInfo, throwException);
			}
			catch (CatalogException)
			{
				if (throwException)
					throw;
			}
			catch (Exception e)
			{
				if (throwException)
					throw new CatalogException("Unknown exception thrown while getting catalog entry. See innerException", e);
			}
			return new object[] { val , nameInfo };
		}

		public virtual object find(NameInfo nameInfo, bool throwException)
		{
			return find(nameInfo, throwException, false);
		}

		public virtual object find(NameInfo nameInfo, bool throwException, bool returnXmlContentOnly)
		{
			object val = null;

			IApplication app = Application.currentApp;

			string name = nameInfo.CatalogLocationName + nameInfo.CatalogEntryName;
			if (nameInfo.CatalogEntryName != null)
			{
				if (returnXmlContentOnly || (val = m_map.get(nameInfo.CatalogCacheKey)) == null)
				{
					FileInfo f = getCatalogFileInfo(name);
					if (f.Exists)
					{
						LineInfoXmlDocument xmlDoc;
						try
						{
							xmlDoc = new LineInfoXmlDocument();
							string fileSysFileName = f.FullName.Replace(@"\\", @"").Replace(@"\", @"/");
							string fileName = "filecrypto://" + fileSysFileName;
							xmlDoc.Load(fileName);
							nameInfo.FileSystemFullFileName = fileSysFileName;
							if (returnXmlContentOnly)
								return xmlDoc.DocumentElement;
						}
						catch (Exception e)
						{
							if (throwException)
								throw new CatalogException(
									String.Format("Cannot load XML for catalog entry '{0}'", name), e);
							else 
								return null;
						}

						if (m_usingSpecifiedClassFactory)
						{
							IClassFactory factory = app.factories[m_className];
							if (factory == null)
							{
								if (throwException)
									throw new CatalogException(
										String.Format("Cannot locate factory named '{0}' for catalog entry '{1}'", m_className, name));
								else
									return null;
							}
							string className = xmlDoc.DocumentElement.Name;
							try
							{
								val = factory.createFromPattern(className, xmlDoc.DocumentElement, nameInfo);
							}
							catch (Exception)
							{
								try
								{
									val = factory.createFromPattern(className, xmlDoc.DocumentElement, name);
								}
								catch (Exception e)
								{
									if (throwException)
										throw new CatalogException(String.Format(
											"Unable to create an instance of '{0}' from the factory '{1}' for catalog entry", className, m_className, name), e);
									else
										return null;
								}
							}
						}
						else if(m_className.Length != 0)
						{
							object[] callParams = null;
							ConstructorInfo theConst = null;
							Type t = Cmn.findType(m_className);
							ConstructorInfo[] cs = t.GetConstructors();
							foreach (ConstructorInfo c in cs)
							{
								ParameterInfo[] ps = c.GetParameters();
								if (ps.Length == 2 && ps[1].ParameterType == typeof(NameInfo))
								{
									theConst = c;
									callParams = new object[] { xmlDoc.DocumentElement, nameInfo };
									break;
								}
							}
							if (theConst == null)
								foreach (ConstructorInfo c in cs)
								{
									ParameterInfo[] ps = c.GetParameters();
									if (ps.Length == 2 && ps[1].ParameterType == typeof(string))
									{
										theConst = c;
										callParams = new object[] { xmlDoc.DocumentElement, name };
										break;
									}
								}
                            if (theConst == null)
                                foreach (ConstructorInfo c in cs)
                                {
                                    ParameterInfo[] ps = c.GetParameters();
                                    if (ps.Length == 1 && ps[0].ParameterType == typeof(XmlElement))
                                    {
                                        theConst = c;
                                        callParams = new object[] { xmlDoc.DocumentElement };
                                        break;
                                    }
                                }
                            
                            if (theConst == null)
							{
								if (throwException)
									throw new CatalogException(String.Format(
										"Could not find a suitable constructor on '{0}' for catalog entry '{1}'", m_className, name));
								else
									return null;
							}
							else
							{

								try
								{
									val = theConst.Invoke(callParams);
								}
								catch (Exception e)
								{
									if (throwException)
										throw new CatalogException(String.Format(
											"Unable to create an instance of '{0}' for catalog entry '{1}'", m_className, name), e);
									else
										return null;
								}
							}
						}

						if(val == null)
							val = xmlDoc.DocumentElement;

						bool isCacheableEntry = true;
						ICatalogItem info = val as ICatalogItem;
						if (info != null)
						{
							info.catalogName = m_catalogName;
							info.expiration = "expire on change of " + f.FullName;
							addDefaultExpire(app, info);
							info.key = name;
							info.searchPath = f.FullName;

							isCacheableEntry = info.isCacheable;
						}

						if (isCacheableEntry)
						{
							cacheEntry(f.FullName, nameInfo.CatalogCacheKey, val);
						}
					}
					else if(throwException)
						throw new CatalogException(String.Format("Cannot find catalog entry '{0}' in catalog", name));
				}
			}

			return val;
		}

		public NameInfo calculateNameInfo(string name)
		{
			string featureName = "";
			StringBuilder dirName = new StringBuilder(255);
			StringBuilder keyName = new StringBuilder(255);
			NameInfo results = null;
			string curName = "";
			if (name != null)
			{
				string[] parts = name.Split('.');
				if (m_type == FileType.Directory)
				{
					DirectoryInfo dinfo = m_base as DirectoryInfo;
					Debug.Assert(dinfo != null);

					int start = 0;
					int numParts = 1;
					while (start+numParts <= parts.Length)
					{
						curName = createNameParts(parts, start, numParts);
						DirectoryInfo[] dirs = dinfo.GetDirectories(curName);
						if (dirs.Length != 0)
						{
							if (dirName.Length == 0)
								featureName = curName;
							dirName.Append(curName).Append("\\");
							keyName.Append(curName).Append("_");
							dinfo = dirs[0];
							start += numParts;
							numParts = 1;
						}
						else
						{
							FileInfo f = getCatalogFileInfo(curName, dinfo);
							if (f.Exists)
							{
								start += numParts;
								keyName.Append(curName);
								results = new NameInfo(keyName.Replace(".","_").ToString(), dirName.ToString(), 
									curName, createNameParts(parts, start, parts.Length - start), featureName);
								break;
							}
							else
								++numParts;
						}
					}
				}
			}

			return results != null ? results :
				new NameInfo(keyName.Replace(".", "_").ToString(), dirName.ToString(), curName, null, featureName);
		}

		protected string createNameParts(string[] parts, int start, int numParts)
		{
			if (numParts == 0)
				return "";
			if (numParts == 1)
				return parts[start];
			int end = start + numParts;
			StringBuilder buff = new StringBuilder(255);
			for (int x = start; x < end; ++x)
				buff.Append(parts[x]).Append(".");
			return buff.ToString(0, buff.Length - 1);
		}

		protected void addDefaultExpire(IApplication app, ICatalogItem info)
		{
			//Get the default time expire info for catalogs
			string defaultCatalogTimeExpire = app.resMgr.resolve("$CatalogDefaultTimeExpire$");
			if (!Cmn.IsEmpty(defaultCatalogTimeExpire) && !defaultCatalogTimeExpire.Equals("$CatalogDefaultTimeExpire$"))
			{
				//If the macro is set, add the time expire onto the node.
				info.expiration=defaultCatalogTimeExpire + ";" + info.expiration;
			}

		}
		protected FileInfo getCatalogFileInfo(string name, DirectoryInfo baseLocation)
		{
			string locName = baseLocation == null ? "" : baseLocation.FullName;

			FileInfo f = new FileInfo(Path.Combine(locName, name + ".enc"));
			if (!f.Exists)
				f = new FileInfo(Path.Combine(locName, name + ".xml"));
			return f;
		}
		protected FileInfo getCatalogFileInfo(string name)
		{
			return getCatalogFileInfo(name, m_base as DirectoryInfo);
		}

		public bool isGetOnly {get {return true;}}

		public XmlSerializationSupport SerializationSupport { get{	return XmlSerializationSupport.None;}}

		public string Xml {get {return null;}}

		public string getXml(string options) {return null;}

		public virtual bool containsField(String nameAddress)
		{
			if(m_type == FileType.Directory)
				return File.Exists(Path.Combine(m_base.FullName, nameAddress + ".xml"));
			return false;
		}

		#endregion

		#region IEnumerable Members

		IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return new XmlCatalogContainerNodeEnumerator(this,m_map,m_base,m_type);
		}

		#endregion
		#region Enumerator Code

		protected class XmlCatalogContainerNodeEnumerator : IDictionaryEnumerator
		{
			protected IDictionaryEnumerator m_enum;
			public XmlCatalogContainerNodeEnumerator(XmlCatalogContainerNode parent,FlexiMap map, FileSystemInfo baseInfo,FileType typeInfo)
			{
				FlexiMap newMap = new FlexiMap();
				if (typeInfo == FileType.Directory)
				{
					DirectoryInfo dirInfo = (DirectoryInfo) baseInfo;
					FileInfo[] files = dirInfo.GetFiles(@"*.xml");
					for (int x=0; x < files.Length ; x++)
					{
						FileInfo file = files[x];
						newMap.put(file.Name,parent.get(file.Name,null));
					}
				}
				else if (typeInfo == FileType.File)
				{
					parent.get(((FileInfo) baseInfo).Name,null);
				}
				if (((IDictionary)map).Count != 0)
				{
					m_enum = ((IDictionary)map).GetEnumerator();
				}
				else
				{
					m_enum = ((IDictionary) newMap).GetEnumerator();
				}
				
			}
			#region IEnumerator Members

			public void Reset()
			{
				m_enum.Reset();
			}

			public object Current
			{
				get
				{
					return m_enum.Current;
				}
			}

			public bool MoveNext()
			{
				return m_enum.MoveNext();
			}
			public DictionaryEntry Entry {get{return m_enum.Entry;}}
			public object Key {get{return m_enum.Key;}}
			public object Value {get{return m_enum.Value;}}
			#endregion
		}
		#endregion
		protected enum FileType {Directory, File, NotExist};
		protected FileType getType(string path) 
		{
			FileInfo f = new FileInfo(path);
			DirectoryInfo d = new DirectoryInfo(path);
			if(!f.Exists && d.Exists)
				return FileType.Directory;
			else if(f.Exists && !d.Exists)
				return FileType.File;
			else
				return FileType.NotExist;
		}
		
		#region IDictionary Members

		public bool IsReadOnly
		{
			get
			{
				return true;
			}
		}

		IDictionaryEnumerator System.Collections.IDictionary.GetEnumerator()
		{
			return new XmlCatalogContainerNodeEnumerator(this,m_map,m_base,m_type);
		}

		public object this[object key]
		{
			get
			{
				return this.get(key.ToString(),null);
			}
			set
			{
				// TODO:  Throw some type exception since this is readonly.
			}
		}

		public void Remove(object key)
		{
			// TODO:  Throw some type exception since this is readonly.
		}

		public bool Contains(object key)
		{
			return this.containsField(key.ToString());
		}

		public void Clear()
		{
			((IDictionary) m_map).Clear();
		}

		public ICollection Values
		{
			get
			{
				return ((IDictionary) m_map).Values;
			}
		}

		public void Add(object key, object value)
		{
			// TODO:  Throw some type exception since this is readonly.
		}

		public ICollection Keys
		{
			get
			{
				return ((IDictionary) m_map).Keys;;
			}
		}

		public bool IsFixedSize
		{
			get
			{
				return true;
			}
		}

		#endregion

		#region ICollection Members

		public bool IsSynchronized
		{
			get
			{
				return false;
			}
		}

		public int Count
		{
			get
			{
				if (m_type == FileType.Directory)
				{
					DirectoryInfo dirInfo = (DirectoryInfo) m_base;
					FileInfo[] files = dirInfo.GetFiles(@"*.xml");
					return files.Length;
				}
				else if (m_type == FileType.File)
				{
					return 1;
				}
				else
				{
					return 0;
				}
				
			}
		}

		public void CopyTo(Array array, int index)
		{
			// TODO:  Add XmlCatalogContainerNode.CopyTo implementation
		}

		public object SyncRoot
		{
			get
			{
				return ((ICollection) m_map).SyncRoot;
			}
		}

		#endregion

		/// <summary>
		/// When breaking down the name for a catalog, the deep name given is broken
		/// down into pieces that are needed by various places in XmlCatalog.  Also, this
		/// information can be passed to the constructor the catalog entry class rather then just
		/// a simple string "catalogName".</summary>
		public class NameInfo
		{
			/// <summary>
			/// The key to use for storing the catalog entry object in the cache.</summary>
			public string CatalogCacheKey;
			/// <summary>
			/// The sub directory or other location the catalog came from within the base
			/// directory or location of the catalog itself.</summary>
			public string CatalogLocationName;
			/// <summary>
			/// The actual name of the catalog entry.</summary>
			public string CatalogEntryName;
			/// <summary>
			/// Any other names after the catalog entry that should be passed onto the
			/// entry itself in a get if the object is also an INameAddressSupport.</summary>
			public string RemainingName;
			/// <summary>
			/// If the catalog entry was located within a location other then the root
			/// location of the catalog itself, the feature name is the first "directory"
			/// of the subdirectory, which should be the name of a feature.
			/// </summary>
			public string EntryFeatureName;

			public string FileSystemFullFileName = null;

			public NameInfo(string cacheKey, string locationName, string entryName, string remainingName, string featureName)
			{
				CatalogCacheKey = cacheKey;
				CatalogLocationName = locationName;
				CatalogEntryName = entryName;
				RemainingName = remainingName;
				EntryFeatureName = featureName;
			}
		}
	}

}
